package core;

import com.alibaba.fastjson.JSON;
import data.cad.CAD;
import data.component.BoundingBox;
import data.component.Line;
import data.component.Wall;
import data.xml.XML;
import org.apache.commons.io.FileUtils;
import org.opencv.core.*;
import utils.Geometry;
import utils.Matrix;

import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;

import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import utils.Vector;
import utils.Vector3;

enum Alignment {
    Start, End, Mid;
}

public class SemanticMatch {
    private final int PIXEL = 64;

    private final int LINEWIDTH = 2;

    public Matrix matrix = Matrix._identity();

    public double credibility;

    public HashMap<String, Wall> mapping = new HashMap<String, Wall>();

    public Boolean sameDirection = true;

    static class DSTransform {
        /**
         * 降采样变换
         */
        public Double xOffset;
        public Double yOffset;
        public Double scale;

        public DSTransform(Double x, Double y, Double s) {
            xOffset = x;
            yOffset = y;
            scale = s;
        }
    }

    interface Match
    {
        void execute(Alignment alignment);
    }


    public SemanticMatch(CAD cad, XML xml, String cadId, String xmlId) {
        var cre2Mtr = new HashMap<Double, Matrix>();

        for (var cadWall : cad.filteredWalls) {
            for (var xmlWall : xml.separatedWalls) {
                //手动匹配 给出对应墙id的情况
                if (cadId != null && xmlId != null)
                    if (!Objects.equals(cadWall.id, cadId) || !Objects.equals(xmlWall.id, xmlId))
                        continue;

                // 自动匹配墙面长度差距超1.5倍的滤去
                if ((cadId == null && xmlId == null) && (Math.max(cadWall.length, xmlWall.length) /
                        Math.min(cadWall.length, xmlWall.length) > 1.5))
                    continue;

                // 根据墙与扫描点计算对应的顺时针方向线
                var xmlCwLine = GetClockwiseLine(xmlWall, new Double[] { 0d, 0d });
                var cadCwLine = GetClockwiseLine(cadWall, cad.station.location);

                Match func = (Alignment alignment)-> {
                    var mtr = GetMatrixByCwLine(xmlCwLine, cadCwLine, alignment);
                    var cre = CalcCredibility(cad, xml, mtr);
                    cre2Mtr.put(cre, mtr);
                };

                func.execute(Alignment.Start);

                func.execute(Alignment.Mid);

                func.execute(Alignment.End);
            }

            var creList =new ArrayList<>(cre2Mtr.keySet()) ;

            this.credibility = Collections.max(creList);

            this.matrix = cre2Mtr.get(this.credibility);

            GetMappingByDownSampling(cad, xml);
        }
        if(cad.stage.equals("Architecture"))
            this.sameDirection = CheckSameDirectional(cad.room.contours, xml.boundingBox, matrix);
    }

    public SemanticMatch(CAD cad, XML xml, String preMatchPath)
    {
        File file = new File(preMatchPath);
        String fileString = null;
        try {
            fileString = FileUtils.readFileToString(file, "utf-8");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        var preMatchJson = JSON.parseObject(fileString);
        var MatrixValues = preMatchJson.getObject("Matrix", Double[].class);
        this.matrix = new Matrix(MatrixValues);
        this.credibility = 1;
        GetMappingByDownSampling(cad, xml);

    }

    /**
     * 根据平移缩放关系，画出位图，xml需要坐标变换，cad不需要
     * @param walls cad或xml中的墙
     * @param xOffset x坐标平移
     * @param yOffset y坐标平移
     * @param scale 缩放
     * @param needTransformed xml需要坐标变换，cad不需要
     * @return
     */
    private Mat PaintImg(List<Wall> walls, Double xOffset, Double yOffset, Double scale, boolean needTransformed) {

        var img = new Mat(PIXEL, PIXEL, CvType.CV_8UC3, new Scalar(0, 0, 0));
        for (var w : walls) {
            var start = needTransformed ? matrix.Transform(w.start) : w.start;
            var end = needTransformed ? matrix.Transform(w.end) : w.end;
            var lStart = new Point((int) Math.round((start[0] - xOffset) * scale) + 1,
                    (int) Math.round((start[1] - yOffset) * scale) + 1);
            var lEnd = new Point((int) Math.round((end[0] - xOffset) * scale) + 1,
                    (int) Math.round((end[1] - yOffset) * scale) + 1);
            Imgproc.line(img, lStart, lEnd, new Scalar(255, 255, 255), LINEWIDTH);
        }

        return img;
    }

    /**
     * 坐标变换到同一坐标系后，计算降采样图像的重合率来确定匹配可信度
     * @param cad
     * @param xml
     * @param mtr 坐标变换矩阵
     * @return 可信度
     */
    private Double CalcCredibility(CAD cad, XML xml, Matrix mtr) {
        this.matrix = mtr;
        var t = GetDownSamplingTransform(xml.boundingBox, cad.boundingBox);

        var cadImg = PaintImg(cad.filteredWalls, t.xOffset, t.yOffset, t.scale, false);
        var xmlImg = PaintImg(xml.separatedWalls, t.xOffset, t.yOffset, t.scale, true);

        // HighGui.ImShow("cadImg", cadImg);
        // HighGui.ImShow("xmlImg", xmlImg);
        // HighGui.WaitKey();
        var intersection = new Mat(PIXEL, PIXEL, CvType.CV_8UC3, new Scalar(0, 0, 0));
        Core.bitwise_and(cadImg, xmlImg, intersection);
        var cadNonZero = Geometry.CountNonZeroPixel(cadImg);
        var xmlNonZero = Geometry.CountNonZeroPixel(xmlImg);

        if (Math.min(cadNonZero, xmlNonZero) == 0)
            return 0.0d;

        return Geometry.CountNonZeroPixel(intersection) / Math.min(cadNonZero, xmlNonZero);
    }

    /**
     * 计算统一坐标系后，存入PIXEL*PIXEL大小的位图所需要做的坐标平移和缩放的值
     * @param pre 需要变化坐标系的BoundingBox pre
     * @param post 目标的基准坐标系下的BoundingBox post
     * @return x,y轴平移量 scale 缩放量
     */
    private DSTransform GetDownSamplingTransform(BoundingBox pre, BoundingBox post) {
        var bBox = MergeBoundingBox(post, pre);
        var xOffset = bBox.minX - LINEWIDTH;
        var yOffset = bBox.minY - LINEWIDTH;
        var length = Math.max(bBox.size[0], bBox.size[1]);
        var scale = (PIXEL - LINEWIDTH - 2) / length;
        return new DSTransform(xOffset, yOffset, scale);
    }

    /**
     * 融合同一坐标系下的bounding box
     * @param noNeedTBox cad的boundingBox 不需要坐标变换
     * @param needTBox xml的boundingBox 需要坐标变换
     * @return 合并后的boundingBox
     */
    private BoundingBox MergeBoundingBox(BoundingBox noNeedTBox, BoundingBox needTBox) {
        var tPoints = new ArrayList<>(Arrays.asList(matrix.Transform(needTBox.min), matrix.Transform(needTBox.max)));

        var tXmlBox = new BoundingBox(tPoints);

        return BoundingBox.Merge(noNeedTBox, tXmlBox);
    }

    /**
     * 通过指定的对应的墙的顺时针方向线和对齐方式来计算矩阵，（平移，旋转，无缩放）
     * @param xmlCwLine xml墙对应的顺时针方向线
     * @param cadCwLine cad墙对应的顺时针方向线
     * @param alignment 对齐方向
     * @return 坐标变换矩阵
     */
    private Matrix GetMatrixByCwLine(Line xmlCwLine, Line cadCwLine, Alignment alignment) {
        var translation = GetTranslation(xmlCwLine, cadCwLine, alignment);
        var mTranslation = Matrix.CreateTranslation(translation.x, translation.y, translation.z);

        var angle = GetAngleByCwLine(xmlCwLine, cadCwLine);
        var mRotationZ = Matrix.CreateRotationZ(angle);
        var t = Matrix.Multiply(mTranslation,mRotationZ) ;
        // var scale = v2.Length() / v1.Length();
        // t = t * Matrix4x4.CreateScale(scale);
        return t;
    }

    /**
     * 根据顺时针方向线获取两线夹角, pre->post
     * @param pre
     * @param post
     * @return
     */
    private double GetAngleByCwLine(Line pre, Line post) {
        var v1 = new Vector(pre.start, pre.end);
        var v2 = new Vector(post.start, post.end);
        return Vector.CalcClockWiseAngle(v1, v2);
    }

    /**
     * 墙 start -> end 调整成相对于扫描点的顺时针方向
     * @param line
     * @param scanPoint
     * @return
     */
    private Line GetClockwiseLine(Line line, Double[] scanPoint) {
        var vecStart = new Vector(scanPoint, line.start);
        var vecEnd = new Vector(scanPoint, line.end);
        var theta = Vector.CalcClockWiseAngle(vecEnd, vecStart);
        return theta < Math.PI
                ? new Line(line.start, line.end, true)
                : new Line(line.end, line.start, true);
    }

    /**
     * 根据对齐方式计算平移量
     * @param pre
     * @param post
     * @param alignment
     * @return
     */
    private static Vector3 GetTranslation(Line pre, Line post, Alignment alignment) {
        switch (alignment) {
            case Start:
                return new Vector3(-pre.start[0] + post.start[0], -pre.start[1] + post.start[1], 0d);
            case Mid:
                return new Vector3(-pre.center[0] + post.center[0], -pre.center[1] + post.center[1], 0d);
            case End:
                return new Vector3(-pre.end[0] + post.end[0], -pre.end[1] + post.end[1], 0d);
        }

        return new Vector3(0d, 0d, 0d);
    }

    private void GetMappingByDownSampling(CAD cad, XML xml)
    {
        var t = GetDownSamplingTransform(xml.boundingBox, cad.boundingBox);

        var xmlWalls = xml.subWalls.isEmpty()? xml.separatedWalls :xml.subWalls;

        for (var cadWall : cad.walls)
        {

            var cadImg = PaintImg(Collections.singletonList(cadWall), t.xOffset, t.yOffset, t.scale, false);
            var coverageRatio = new ArrayList<Double>();
            for (var xmlWall : xmlWalls)
            {
                //  var angle = GetAngleByCwLine(xmlWall, cadWall);
                // 两线同向 夹角小于 30度  或 两线反向 夹角小于 30 度 则继续，反之夹角过大则滤去
                // if ((Math.Abs(angle) > Math.PI / 6) || Math.Abs(angle - Math.PI) > Math.PI / 6)
                // {
                //     coverageRatio.Add(0);
                //     continue;
                // }

                var xmlImg = PaintImg(Collections.singletonList(xmlWall), t.xOffset, t.yOffset, t.scale, true);
                var intersection = new Mat(PIXEL, PIXEL, CvType.CV_8UC3, new Scalar(0, 0, 0));
                Core.bitwise_and(cadImg, xmlImg, intersection);
                if (Math.min(Geometry.CountNonZeroPixel(cadImg), Geometry.CountNonZeroPixel(xmlImg)) == 0)
                {
                    coverageRatio.add(0d);
                    continue;
                }
                coverageRatio.add(Geometry.CountNonZeroPixel(intersection) /
                        Math.min(Geometry.CountNonZeroPixel(cadImg), Geometry.CountNonZeroPixel(xmlImg)));
            }

            if (Collections.max(coverageRatio)< 0.3)
                mapping.put(cadWall.id, null);
            else
            {
                mapping.put(cadWall.id, xmlWalls.get(coverageRatio.indexOf(Collections.max(coverageRatio))));
            }

        }
    }

    /**
     * 判断房间的长边与xml的长边同向
     * @param roomContour 房间轮廓点集
     * @param xmlBBox xml BoundingBox
     * @param matrix 用于将xml变换到room的坐标系中的矩阵
     * @return 长边是否同向
     */
    private Boolean CheckSameDirectional(ArrayList<Double[]> roomContour, BoundingBox xmlBBox, Matrix matrix)
    {
        // 将xml的边框轮廓变换到room的同一个坐标系
        var TXmlContours  = (new ArrayList<>(Arrays.asList(xmlBBox.max, xmlBBox.min, xmlBBox.leftUp, xmlBBox.rightDw))).stream().map(matrix::Transform).collect(Collectors.toList());

        var TXmlBBox = new BoundingBox(TXmlContours);
        var xmlLongDirection = (TXmlBBox.maxX - TXmlBBox.minX) > (TXmlBBox.maxY - TXmlBBox.minY) ? "X" : "Y";

        var roomBBox = new BoundingBox(roomContour);
        var roomLongDirection = (roomBBox.maxX - roomBBox.minX) > (roomBBox.maxY - roomBBox.minY) ? "X" : "Y";

        return xmlLongDirection.equals(roomLongDirection);
    }

}

